﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

// Based on the C# Language Specification 5.0
// http://www.microsoft.com/en-us/download/confirmation.aspx?id=7029


namespace MetaSharp.Transformation.Grammar
{
    public class Tokenizer
    {
        private static Pattern _newLine;
        private static Pattern _newLineCharacter;
        private static Pattern _whitespace;
        private static Pattern _unicodeEscapeSequence;
        private static Pattern _hexDigit;
        private Pattern _comment;
        private Pattern _singleLineComment;
        private Pattern _singleLineCommentStart;
        private Pattern _inputCharacters;
        private Pattern _inputCharacter;
        private Pattern _delimitedComment;
        private Pattern _delimitedCommentText;
        private Pattern _delimitedCommentSection;
        private Pattern _asterisks;
        private Pattern _notSlashOrAsterisk;
        private Pattern _identifier;
        private Pattern _availableIdentifier;
        private Pattern _identifierOrKeyword;
        private Pattern _identifierStartCharacter;
        private Pattern _identifierPartCharacters;
        private Pattern _identifierPartCharacter;
        private Pattern _letterCharacter;
        private Pattern _combiningCharacter;
        private Pattern _decimalDigitCharacter;
        private Pattern _connectingCharacter;
        private Pattern _formattingCharacter;
        private Pattern _keyword;

        
        private static readonly string[] Keywords = new [] {
            "abstract", "as", "base", "bool", "break",
            "byte", "case", "catch", "char", "checked",
            "class", "const", "continue", "decimal", "default",
            "delegate", "do", "double", "else", "enum",
            "event", "explicit", "extern", "false", "finally",
            "fixed", "float", "for", "foreach", "goto",
            "if", "implicit", "in", "int", "interface",
            "internal", "is", "lock", "long", "namespace",
            "new", "null", "object", "operator", "out",
            "override", "params", "private", "protected", "public",
            "readonly", "ref", "return", "sbyte", "sealed",
            "short", "sizeof", "stackalloc", "this", "throw", "true",
            "try", "typeof", "uint", "ulong", "unchecked",
            "unsafe", "ushort", "using", "virtual", "void",
            "volatile", "while"
        };

        static Tokenizer()
        {
            _newLine = Pattern.Rule(
                Pattern.Or(
                    Pattern.And(
                        Pattern.Value('\u000D'),
                        Pattern.Value('\u000A')),
                    Pattern.Reference(() => NewLineCharacter)));

            _newLineCharacter = Pattern.Rule(
                Pattern.Or(
                    Pattern.Value('\u000D'),
                    Pattern.Value('\u000A'),
                    Pattern.Value('\u0085'),
                    Pattern.Value('\u2028'),
                    Pattern.Value('\u2029')));

            // any character with Unicode class Zs
            _whitespace = Pattern.Rule(new UnicodeCategoryPattern(UnicodeCategory.SpaceSeparator));

            _unicodeEscapeSequence = Pattern.Rule(
                Pattern.Or(
                    Pattern.And(
                        Pattern.Value('\\'),
                        Pattern.Value('u'),
                        Pattern.ExactQuantity(Pattern.Reference(() => HexDigit), 4)),
                    Pattern.And(
                        Pattern.Value('\\'),
                        Pattern.Value('U'),
                        Pattern.ExactQuantity(Pattern.Reference(() => HexDigit), 8))));

            _hexDigit = Pattern.Rule(
                Pattern.Or(
                    Pattern.Range('0', '9'),
                    Pattern.Range('a', 'f'),
                    Pattern.Range('A', 'F')));
        }

        public Tokenizer()
        {
            _comment = Pattern.Rule(
                Pattern.Or(
                    Pattern.Reference(() => SingleLineComment),
                    Pattern.Reference(() => DelimitedComment)));

            _singleLineComment = Pattern.Rule(
                Pattern.And(
                    Pattern.Reference(() => SingleLineCommentStart),
                    Pattern.Optional(Pattern.Reference(() => InputCharacters))));

            _singleLineCommentStart = Pattern.Rule(
                Pattern.And(
                    Pattern.Value('/'),
                    Pattern.Value('/')));

            _inputCharacters = Pattern.Rule(
                Pattern.Or(
                    Pattern.And(
                        Pattern.Reference(() => InputCharacters),
                        Pattern.Reference(() => InputCharacter)),
                    Pattern.Reference(() => InputCharacter)));

            _inputCharacter = Pattern.Rule(
                Pattern.And(
                    Pattern.Not(Pattern.Reference(() => NewLineCharacter)),
                    Pattern.Range('\u0000', '\uFFFF')));

            _delimitedComment = Pattern.Rule(
                Pattern.And(
                    Pattern.Value('/'),
                    Pattern.Value('*'),
                    Pattern.Optional(Pattern.Reference(() => DelimitedCommentText)),
                    Pattern.Reference(() => Asterisks),
                    Pattern.Value('/')));

            _delimitedCommentText = Pattern.Rule(
                Pattern.Or(
                    Pattern.And(
                        Pattern.Reference(() => DelimitedCommentText),
                        Pattern.Reference(() => DelimitedCommentSection)),
                    Pattern.Reference(() => DelimitedCommentSection)));


            _delimitedCommentSection = Pattern.Rule(
                Pattern.Or(
                    Pattern.Value('/'),
                    Pattern.And(
                        Pattern.Optional(Pattern.Reference(() => Asterisks)),
                        Pattern.Reference(() => NotSlashOrAsterisk))));

            _asterisks = Pattern.Rule(
                Pattern.Or(
                    Pattern.And(
                        Pattern.Reference(() => Asterisks),
                        Pattern.Value('*')),
                    Pattern.Value('*')));

            _notSlashOrAsterisk = Pattern.Rule(
                Pattern.And(
                    Pattern.Not(Pattern.Value('/')),
                    Pattern.Not(Pattern.Value('*')),
                    Pattern.Range('\u0000', '\uffff')));

            _identifier = Pattern.Rule(
                Pattern.Or(
                    Pattern.Reference(() => AvailableIdentifier),
                    Pattern.And(
                        Pattern.Value('@'),
                        Pattern.Reference(() => IdentifierOrKeyword))));

            // private rule AvailableIdentifier = 
            //     i:(i:IdentifierOrKeyword -> Tokenizer.Stringify(i)) (!Keyword)(i)) -> i
            _availableIdentifier = Pattern.Rule(
                Pattern.Production(
                    Pattern.And(
                        Pattern.Variable(
                            "i",
                            Pattern.Production(
                                Pattern.Variable(
                                    "i",
                                    Pattern.Reference(() => IdentifierOrKeyword)),
                                (object i) => Tokenizer.Stringify(i))),                                
                        Pattern.Switch(
                            "i",
                            Pattern.Not(Pattern.Reference(() => Keyword)))),
                    (string i) => i));

            _identifierOrKeyword = Pattern.Rule(
                Pattern.And(
                    Pattern.Reference(() => IdentifierStartCharacter),
                    Pattern.Optional(
                        Pattern.Reference(() => IdentifierPartCharacters))));

            _identifierStartCharacter = Pattern.Rule(
                Pattern.Or(
                    Pattern.Reference(() => LetterCharacter),
                    Pattern.Value('_')));

            _identifierPartCharacters = Pattern.Rule(
                Pattern.Or(
                    Pattern.And(
                        Pattern.Reference(() => IdentifierPartCharacters),
                        Pattern.Reference(() => IdentifierPartCharacter)),
                    Pattern.Reference(() => IdentifierPartCharacter)));

            _identifierPartCharacter = Pattern.Rule(
                Pattern.Or(
                    Pattern.Reference(() => LetterCharacter),
                    Pattern.Reference(() => DecimalDigitCharacter),
                    Pattern.Reference(() => ConnectingCharacter),
                    Pattern.Reference(() => CombiningCharacter),
                    Pattern.Reference(() => FormattingCharacter)));
            
            // Lu, Ll, Lt, Lm, Lo, or Nl 
            var unicodeLetterPattern = new UnicodeCategoryPattern(
                UnicodeCategory.UppercaseLetter,
                UnicodeCategory.LowercaseLetter,
                UnicodeCategory.TitlecaseLetter,
                UnicodeCategory.ModifierLetter,
                UnicodeCategory.OtherLetter,
                UnicodeCategory.LetterNumber);

            _letterCharacter = Pattern.Rule(
                Pattern.Or(
                    Pattern.Production(
                        Pattern.And(
                            Pattern.Variable(
                                "u",
                                Pattern.Or(
                                    Pattern.Production(
                                        Pattern.And(
                                            Pattern.Value('\\'),
                                            Pattern.Value('u'),
                                            Pattern.Variable("hex", Pattern.ExactQuantity(Pattern.Reference(() => HexDigit), 4))),
                                        new Func<object[], char>(Hex4ToCharProduction)),
                                    Pattern.Production(
                                        Pattern.And(
                                            Pattern.Value('\\'),
                                            Pattern.Value('U'),
                                            Pattern.Variable("hex", Pattern.ExactQuantity(Pattern.Reference(() => HexDigit), 8))),
                                        new Func<object[], char>(Hex8ToCharProduction)))),
                            Pattern.Switch(
                                "u",
                                unicodeLetterPattern)),
                        (char u) => u),
                    unicodeLetterPattern));

            _combiningCharacter = Pattern.Rule(new UnicodeCategoryPattern(
                UnicodeCategory.NonSpacingMark,
                UnicodeCategory.SpacingCombiningMark));

            _decimalDigitCharacter = Pattern.Rule(new UnicodeCategoryPattern(UnicodeCategory.DecimalDigitNumber));

            _connectingCharacter = Pattern.Rule(new UnicodeCategoryPattern(UnicodeCategory.ConnectorPunctuation));

            _formattingCharacter = Pattern.Rule(new UnicodeCategoryPattern(UnicodeCategory.Format));

            var keywordValuePatterns = new Pattern[Tokenizer.Keywords.Length];
            for (var x = 0; x < keywordValuePatterns.Length; x++)
                keywordValuePatterns[x] = Pattern.Value(Tokenizer.Keywords[x]);

            _keyword = Pattern.Rule(Pattern.Or(keywordValuePatterns));
        }

        private static char Hex4ToCharProduction(object[] hex)
        {
            var s = "" + hex[0] + hex[1] + hex[2] + hex[3];
            var c = (char)int.Parse(s, NumberStyles.HexNumber);
            return c;
        }

        private static char Hex8ToCharProduction(object[] hex)
        {
            var s = "" + hex[0] + hex[1] + hex[2] + hex[3] + hex[4] + hex[5] + hex[6] + hex[7];
            var c = (char)int.Parse(s, NumberStyles.HexNumber);
            return c;
        }

        public Pattern Main
        {
            get { return Pattern.ZeroOrMore(Pattern.Any()); }
        }

        public static Pattern NewLine
        {
            get { return _newLine; }
        }

        public static Pattern NewLineCharacter
        {
            get { return _newLineCharacter; }
        }

        public static Pattern Whitespace
        {
            get { return _whitespace; }
        }

        private static Pattern HexDigit
        {
            get { return _hexDigit; }
        }

        public virtual Pattern Comment
        {
            get { return _comment; }
        }

        public Pattern SingleLineComment
        {
            get { return _singleLineComment; }
        }

        public virtual Pattern SingleLineCommentStart
        {
            get { return _singleLineCommentStart; }
        }

        private Pattern InputCharacters
        {
            get { return _inputCharacters; }
        }

        private Pattern InputCharacter
        {
            get { return _inputCharacter; }
        }

        public virtual Pattern DelimitedComment
        {
            get { return _delimitedComment; }
        }

        private Pattern DelimitedCommentText
        {
            get { return _delimitedCommentText; }
        }

        private Pattern DelimitedCommentSection
        {
            get { return _delimitedCommentSection; }
        }

        private Pattern Asterisks
        {
            get { return _asterisks; }
        }

        private Pattern NotSlashOrAsterisk
        {
            get { return _notSlashOrAsterisk; }
        }

        private Pattern UnicodeEscapeSequence
        {
            get { return _unicodeEscapeSequence; }
        }

        public Pattern Identifier
        {
            get { return _identifier; }
        }

        private Pattern AvailableIdentifier
        {
            get { return _availableIdentifier; }
        }

        private Pattern IdentifierOrKeyword
        {
            get { return _identifierOrKeyword; }
        }

        private Pattern IdentifierStartCharacter
        {
            get { return _identifierStartCharacter; }
        }

        private Pattern IdentifierPartCharacters
        {
            get { return _identifierPartCharacters; }
        }

        private Pattern IdentifierPartCharacter
        {
            get { return _identifierPartCharacter; }
        }

        public Pattern LetterCharacter
        {
            get { return _letterCharacter; }
        }

        public Pattern CombiningCharacter
        {
            get { return _combiningCharacter; }
        }

        public Pattern DecimalDigitCharacter
        {
            get { return _decimalDigitCharacter; }
        }

        public Pattern ConnectingCharacter
        {
            get { return _connectingCharacter; }
        }

        public Pattern FormattingCharacter
        {
            get { return _formattingCharacter; }
        }

        public virtual Pattern Keyword
        {
            get { return _keyword; }
        }

        public static string Stringify(object obj)
        {
            if (obj == null)
                return "";
            if (obj is char)
                return new string((char)obj, 1);
            var value = obj as string;
            if (value != null)
                return value;

            var enumerable = obj as IEnumerable;
            if (enumerable != null)
            {
                var builder = new StringBuilder();
                foreach (var item in enumerable)
                    builder.Append(Stringify(item));
                return builder.ToString();
            }

            return obj.ToString();
        }

        // TODO: Figure out a syntax for allowing people to inject custom patterns like this.
        // First thoughts: Rule = <LetterCharacterPattern>;
        // Which genertes new LetterCharacterPattern() in the location where it is referenced.
        class UnicodeCategoryPattern : Pattern
        {
            private List<UnicodeCategory> categories;
            public UnicodeCategoryPattern(params UnicodeCategory[] categories)
                // TODO: This should be PatternKind.Custom
                : base(PatternKind.Default)
            {
                this.categories = new List<UnicodeCategory>(categories);
            }

            public override PatternMatch Compile()
            {
                return LetterCharacter;
            }

            private IMatch LetterCharacter(IScope scope)
            {
                if (!scope.Stream.IsEmpty)
                {
                    char c = '\0';
                    bool ok = false;
                    var next = scope.Stream.Next();
                    if (next.Current is char)
                    {
                        c = (char)next.Current;
                        ok = true;
                    }
                    else if (next.Current is string)
                    {
                        var s = (string)next.Current;
                        ok = char.TryParse("\\u" + s, out c);
                    }

                    if(ok)
                    {
                        var category = char.GetUnicodeCategory(c);
                        if (this.categories.Contains(category))
                        {
                            return new MetaSharp.Transformation.Match(c, scope, scope.SetStream(next));
                        }
                    }
                }

                return MetaSharp.Transformation.Match.Fail(scope);
            }
        }
    }

    public class Token
    {
    }

    public class Identifier
    {
    }

    public class Keyword
    {
    }

    public class IntegerLiteral
    {
    }

    public class RealLiteral
    {
    }

    public class CharacterLiteral
    {
    }

    public class StringLiteral
    {
    }

    public class Operator
    {
    }

    public class Punctuator
    {
    }
}
